package v5

import (
	"bytes"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/sha256"
	"encoding/gob"
	"fmt"
	"github.com/btcsuite/btcutil/base58"
	"log"
	"math/big"
	mathRand "math/rand"
	"time"
)

const (
	reward = 50.00
)

// 交易结构对象
type Transaction struct {
	//交易id
	TXID []byte
	//交易输出信息
	TxOutputs []TxOutput
	//交易输入信息
	TxInputs []TxInput
}

func GetRandomString(length int) string {
	str := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
	bytes := []byte(str)
	result := []byte{}

	r := mathRand.New(mathRand.NewSource(time.Now().UnixNano()))
	for i := 0; i < length; i++ {
		result = append(result, bytes[r.Intn(len(bytes))])
	}
	return base58.Encode(result)
}

// 设置交易的hash为交易ID
func (t *Transaction) SetHash() {
	var buffer = bytes.Buffer{}
	enc := gob.NewEncoder(&buffer)
	err := enc.Encode(t)
	if err != nil {
		log.Panic(err)
	}
	sum256 := sha256.Sum256(buffer.Bytes())
	t.TXID = sum256[:]
}

// 输出对象
type TxOutput struct {
	//金额
	Value float64
	////锁定脚本
	//ScriptPubKey string
	//公钥哈希
	PublicKeyHash []byte
}

// 锁定脚本
func (output *TxOutput) Lock(address string /*付款⼈的地址*/) {
	//使⽤公钥哈希锁定该output
	output.PublicKeyHash = GetPubKeyHashByAddress(address)
}

func NewTXOutput(value float64, address string) *TxOutput {
	txoutput := TxOutput{value, nil}
	txoutput.Lock(address)
	return &txoutput
}

// 检测是否被某个地址锁定
func (output *TxOutput) CanBeUnlockedWith(pubKeyHash []byte /*收款⼈的公钥哈希*/) bool {
	return bytes.Compare(output.PublicKeyHash, pubKeyHash) == 0
}

// 输入对象
type TxInput struct {
	//关联的上一笔的交易id
	TxID []byte
	//关联的上一笔的输出的索引值(第几笔输出)
	Index int64
	////解锁脚本-签名
	//Sig string
	//签名
	Signature []byte
	//公钥,椭圆曲线X,Y坐标的拼接,并非真正的椭圆曲线的公钥,不是公钥hash
	PublicKey []byte
}

// 解锁脚本
// 存储在output中的其实是公钥的哈希，我们去锁定的时候也是输⼊地址，然后内部逆向算出哈希存储的
func (input *TxInput) CanUnlockUTXOWith(pubKeyHash []byte /*收款⼈的公钥哈希*/) bool {
	hash := GetRipemd160SumPubKeyHash(input.PublicKey)
	return bytes.Compare(hash, pubKeyHash) == 0
}

// 定义一笔挖矿交易
func NewCoinbaseTransaction(addr string, data string) *Transaction {
	fmt.Printf("NewCoinbaseTransaction data:   %s\n", data)
	txOutput := NewTXOutput(reward, addr)
	var txInput = TxInput{
		nil,
		-1,
		nil,
		[]byte(data), //一般为矿池名称
	}
	var t = Transaction{
		nil,
		[]TxOutput{*txOutput},
		[]TxInput{txInput},
	}
	t.SetHash()
	return &t
}

// 是否是挖矿所得的交易
func (t *Transaction) IsCoinbase() bool {
	if len(t.TxInputs) == 1 {
		input := t.TxInputs[0]
		if input.Index == -1 && input.TxID == nil {
			return true
		}
	}
	return false
}

// 定义一笔普通交易
func NewTransaction(from, to string, amount float64, bc *BlockChain) *Transaction {
	//获取转出方的本地钱包=from
	wallet := GetWalletByAddress(from)
	if wallet == nil {
		log.Panic("获取付款人的本地钱包失败,转账失败!")
	}
	//从钱包里面获取公私钥
	publicKey := wallet.PublicKey   // X,Y拼接而成的公钥,不是真正还原的公钥,生成地址也是用的这个,但是校验签名必须使用真正的公钥
	privateKey := wallet.PrivateKey // 真正的私钥-签名用的
	//publicKeyRel := privateKey.PublicKey //真正的公钥 --校验签名用的
	//公钥hash
	pubKeyHash := GetRipemd160SumPubKeyHash(publicKey)
	//获取有效的满足的交易集合
	utxOs, total := bc.FindSuitableUTXOs(pubKeyHash, amount)
	if total < amount {
		fmt.Println("交易失败,余额不足")
		return nil
	}
	var TxInputs []TxInput
	//定义一笔资金接收方=to
	txOutput := NewTXOutput(amount, to)
	var TxOutputs = []TxOutput{*txOutput}
	for UXID, indexArrays := range utxOs {
		for _, index := range indexArrays {
			//定义一笔资金转出方=from
			var txInput = TxInput{
				[]byte(UXID),
				index,
				nil,       //后续再分别设置
				publicKey, //付款人的公钥(X,Y拼接)
			}
			TxInputs = append(TxInputs, txInput)
		}
	}
	if total > amount {
		//余额转入自身
		txOutput0 := NewTXOutput(total-amount, from)
		TxOutputs = append(TxOutputs, *txOutput0)
	}
	var tx = Transaction{
		nil,
		TxOutputs,
		TxInputs,
	}
	tx.SetHash() //设置TXID
	//在创建时进行签名,在旷工添加区块时进行校验签名
	bc.SignTransaction(&tx, privateKey)
	return &tx
}

// 签名--->[每一笔输入+多笔输出+金额]->做签名
func (tx *Transaction) Sign(privateKey *ecdsa.PrivateKey, prevTXs map[string]Transaction) {
	//挖矿交易不用签名
	if tx.IsCoinbase() {
		return
	}
	//校验引用的上一个输出交易室存在的
	for _, input := range tx.TxInputs {
		if prevTXs[string(input.TxID)].TXID == nil {
			log.Panic("未找到引用的前一个的交易输出ID")
		}
	}
	//复制一份交易:作为辅助来完成签名
	txCopy := tx.TrimmedCopy()
	//再次强调⼀下我们要签名的三部分：
	//- 欲使⽤的utxo中的pubKeyHash（这描述了付款⼈）
	//- 新⽣成的utxo中的pubKeyHash（这描述了收款⼈）
	//- 转账⾦额
	//注意，遍历的是txCopy，⽽不是tx本身
	for index, input := range txCopy.TxInputs {
		prevTX := prevTXs[string(input.TxID)]
		//使⽤input的PublicKey字段暂时存储⼀下这个想要解锁的utxo的公钥哈希
		//这⾥有个坑！！！
		//我⼀直使⽤input.PublicKey来赋值，但是其实range会复制⼀个新的变量，⽽不会修改你遍历的
		//的txCopy， 这⾥⼀定要⼩⼼！！！，所以下⾯的这就要修改如下：
		//input.PublicKey = prevTX.TXOutputs[input.VoutIndex].PublicKeyHash
		txCopy.TxInputs[index].PublicKey = prevTX.TxOutputs[input.Index].PublicKeyHash
		//⾄此三个想要签名的数据都有了, 如何签名？签名⼀般对数据的哈希值进⾏签名
		//要签名的哈希存放在txCopy.TXID中
		txCopy.SetHash()
		fmt.Printf("签名时: txCopy.TXID: %x\n", txCopy.TXID)
		//⼀定要赋值为nil，下⾯交易签名还要⽤的，虽然不⽤这个input了，但是它会污染其他交易
		//在后续校验过程时，也会⽣成txCopy，设置nil双⽅才能与⽣成⼀致的数据
		txCopy.TxInputs[index].PublicKey = nil
		r, s, err := ecdsa.Sign(rand.Reader, privateKey, txCopy.TXID)
		if err != nil {
			fmt.Println("err 签名失败")
			log.Panic(err)
		}
		//签名数据[R,S拼接]
		Signature := append(r.Bytes(), s.Bytes()...)
		//tx的索引和txCopy的是⼀致的
		tx.TxInputs[index].Signature = Signature
		fmt.Printf("签名:%d,%x,%x \n", index, Signature, input.TxID)
	}
}

// 4.在挖矿时校验
// 当⼀笔交易发送到对端时，接收⽅在打包到⾃⼰的区块前，需要先对交易进⾏校验，从⽽保证
// 1. 持有者花费的确实是⾃⼰的钱
// 2. 交易确实是由私钥的持有者发起的
// - 分析
// 校验函数(Verify)依然在Transaction结构中实现，需要三个数据：
// 想要签名的数据
// 数字签名
// 公钥
// 代码如下：
// 校验函数原型如下，由于交易中已经存储了 数字签名 和 公钥 ，所以只需要将引⽤的交易传递进来即
// 可（为了获取引⽤输出的公钥哈希）
func (tx *Transaction) Verify(prevTXs map[string]Transaction) bool {
	if tx.IsCoinbase() {
		return true
	}
	for _, input := range tx.TxInputs {
		if prevTXs[string(input.TxID)].TXID == nil {
			log.Panic("previous transaction is not valid!")
		}
	}
	//为了获取原始的签名数据
	txCopy := tx.TrimmedCopy()
	//获取签名的原始数据(⼀个哈希值)，与签名时步骤⼀样
	//签名时对每⼀个引⽤的input都签名，校验也⼀定是对每⼀个input都校验
	//这⾥要注意for循环的对象，应该是原始的inputs结构，⽽不应该是copy过来的那个
	//与Sign时遍历的不同
	for index, input := range tx.TxInputs {
		prevTX := prevTXs[string(input.TxID)]
		//这个可以为了确保为空再设置⼀次nil
		txCopy.TxInputs[index].Signature = nil
		txCopy.TxInputs[index].PublicKey = prevTX.TxOutputs[input.Index].PublicKeyHash
		txCopy.SetHash()
		fmt.Printf("验证签名时: txCopy.TXID: %x \n", txCopy.TXID)
		//这个也可以再次置空，不过不需要，因为每⼀个index对应的input只使⽤⼀次(这是错误的理解)
		//之所尚未出错，因为我们现在都只引⽤⼀个交易就完成了，没校验多个input
		//⼀定要设置，否则会影响其他交易的校验
		txCopy.TxInputs[index].PublicKey = nil
		//处理签名
		r := big.Int{}
		s := big.Int{}
		sigLen := len(input.Signature)
		r.SetBytes(input.Signature[:(sigLen / 2)])
		s.SetBytes(input.Signature[(sigLen / 2):])
		//处理公钥
		x := big.Int{}
		y := big.Int{}
		keyLen := len(input.PublicKey)
		x.SetBytes(input.PublicKey[:(keyLen / 2)])
		y.SetBytes(input.PublicKey[(keyLen / 2):])
		curve := elliptic.P256()
		rawPubKey := ecdsa.PublicKey{curve, &x, &y}
		if !ecdsa.Verify(&rawPubKey, txCopy.TXID, &r, &s) {
			fmt.Printf("验证签名失败:%d,%x,%x \n", index, input.Signature, input.TxID)
			return false
		}
		fmt.Printf("验证签名通过:%d,%x,%x \n", index, input.Signature, input.TxID)
	}
	return true
}

// 复制一份交易数据
func (tx *Transaction) TrimmedCopy() *Transaction {
	var txInputs []TxInput
	var txOutputs []TxOutput
	for _, input := range tx.TxInputs {
		txInputs = append(txInputs, TxInput{input.TxID, input.Index, nil, nil})
	}
	for _, output := range tx.TxOutputs {
		txOutputs = append(txOutputs, TxOutput{output.Value, output.PublicKeyHash})
	}
	return &Transaction{
		tx.TXID,
		txOutputs,
		txInputs,
	}
}
